/**
 * @author Shen Yi - bm2736892@gmail.com
 */

 
SceneManager = {
	_o3dElem : null,
	_pack : null,
	_client : null,
	_o3d : null,
	
	_loader : null,
	
	_renderNodes : [],
	_mainCamera : null,
	_cameras : [],
	_lights : [],
	_transforms : [],
	_models : [],
	
	_renderNodesPriorityAutoIncrement : 100,
	_clearColor : [0, 0, 0.4 , 1],

	//a map to find which shape the primitive own to
	_primitiveShapeMap : [],
	_shapePrimitiveMap : [],
	_shapeTransformMap : [],
	_transformPrimitiveMap : [],

	_renderCallback : null,
	_renderCallbackList : [],
	
	init : function(o3dElem, pack, client, o3d){
		this._o3dElem = o3dElem;
		this._pack = pack;
		this._client = client;
		this._o3d = o3d;
	},
	
	createMainCamera : function(){
		this._mainCamera = this.createCamera("main_camera", 101);
		return this._mainCamera;
	},
	/**
	 * create a new camera(viewInfo) for the scene, and the engine will automatically create a 
	 * render node for the camera(viewInfo). 
	 * @param {String} name
	 * @param {int} priority not needed but recommanded 
	 * @param {!String} textureName needed when the view is render to a texture
	 * @return {Camera}
	 */
	createCamera : function(name, priority, textureName){
		//create a new render node
		var renderNode = this._pack.createObject("RenderNode");
		if(isset(priority)){
			renderNode.priority = priority;
		}
		else{
			renderNode.priority = this._renderNodesPriorityAutoIncrement;
			this._renderNodesPriorityAutoIncrement ++;
		}
		renderNode.parent = this._client.renderGraphRoot;
		this._renderNodes[name] = renderNode;
		//create a new transform root, so the render node will only render
		//the object in this transform
		var transformRoot = this.createTransform(name);
		transformRoot.parent = this._client.root;
		
		var camera = new Camera(name, this._pack, this._client, this._o3d, transformRoot, renderNode, textureName, this._clearColor);
		this._cameras[name] = camera;
		
		return camera;
	},
	/**
	 * 
	 * @param {String} name
	 * @param {Object} option {position, color, type, attenuation, !target, !aspect}
	 */
	createLight : function(name, option){
		var light = new Light(name, this._pack, this._client, this._o3d, option);
		this._lights[name] = light;
		
		return light;
	},
	
	createSkybox : function(background, size){
		CUBE_FACE_INDICES = [
		  [1, 5, 7, 3],
		  [2, 6, 4, 0],
		  [2, 3, 7, 6],
		  [4, 5, 1, 0],
		  [4, 6, 7, 5],
		  [0, 1, 3, 2]
		];
		
		size = isset(size) ? size : 3000;
		var k = size / 2;

        var cornerVertices = [[-k, -k, -k], [+k, -k, -k], [-k, +k, -k], [+k, +k, -k], [-k, -k, +k], [+k, -k, +k], [-k, +k, +k], [+k, +k, +k]];
        
        var faceNormals = [[+1, +0, +0], [-1, +0, +0], [+0, +1, +0], [+0, -1, +0], [+0, +0, +1], [+0, +0, -1]];
        
        var uvCoords = [[0, 0], [1, 0], [1, 1], [0, 1]];
        
        var vertexInfo = o3djs.primitives.createVertexInfo();
        var positionStream = vertexInfo.addStream(3, o3djs.base.o3d.Stream.POSITION);
        var normalStream = vertexInfo.addStream(3, o3djs.base.o3d.Stream.NORMAL);
        var texCoordStream = vertexInfo.addStream(2, o3djs.base.o3d.Stream.TEXCOORD, 0);
        
        for (var f = 0; f < 6; ++f) {
            var faceIndices = CUBE_FACE_INDICES[f];
            for (var v = 0; v < 4; ++v) {
                var position = cornerVertices[faceIndices[v]];
                var normal = faceNormals[f];
                var uv = uvCoords[v];
                
                // Each face needs all four vertices because the normals and texture
                // coordinates are not all the same.
                positionStream.addElementVector(position);
                normalStream.addElementVector(normal);
                texCoordStream.addElementVector(uv);
                
                // Two triangles make a square face.
                var offset = 4 * f;
                vertexInfo.addTriangle(offset + 0, offset + 1, offset + 2);
                vertexInfo.addTriangle(offset + 0, offset + 2, offset + 3);
            }
        }
		var material = MaterialManager.createMaterialFromShader("SKY_BOX", "media/shaders/skybox.shader");
		
		if(typeof(background) == "string"){
			//TODO: create a cube map sampler
		}else{
			material.getParam("isColor").value = 1;
			material.getParam("color").value = background;
		}
        var skyboxShape = vertexInfo.createShape(this._pack, material);
		var transform = this.createTransform("SKY_BOX");
		transform.addShape(skyboxShape);
		transform.parent = this.getTransform("main_camera");
		return transform;
	},
	
	/**
	 * 
	 * @param {!String} name
	 */
	getCamera : function(name){
		if(isset(name)){
			return this._cameras[name];
		}
		return this._cameras;
	},
	getMainCamera : function(){
		return this._mainCamera;
	},
	
	/**
	 * 
	 * @param {!String} light
	 */
	getLight : function(name){
		if(isset(name)){
			return this._lights[name];
		}
		return this._lights;
	},
	/**
	 * 
	 * @param {!String} name
	 */
	getTransform : function(name){
		if(isset(name)){
			return this._transforms[name];
		}
		return this._transforms;
	},
	/**
	 * 
	 * @param {!String} name
	 */
	getRenderNode : function(name){
		if(isset(name)){
			return this._renderNodes[name];
		}
		return this._renderNodes;
	},
	/**
	 * 
	 * @param {!String} name
	 */
	getPrimitiveShapeMap : function(name){
		if(isset(name)){
			return this._primitiveShapeMap[name];
		}
		return this._primitiveShapeMap;
	},
	/**
	 * 
	 * @param {!String} name
	 */
	getShapePrimitiveMap : function(name){
		if(isset(name)){
			return this._shapePrimitiveMap[name];
		}
		return this._shapePrimitiveMap;
	},
	/**
	 * 
	 * @param {!String} name
	 */
	getShapeTransformMap : function(name){
		if(isset(name)){
			return this._shapeTransformMap[name];
		}
		return this._shapeTransformMap;
	},
	/**
	 * 
	 * @param {String} name
	 */
	getPrimitiveTransformMap : function(name){
		var shapeName = this.getPrimitiveShapeMap(name);
		return this.getShapeTransformMap(shapeName);
	},
	/**
	 * 
	 * @param {!String} name
	 */
	getTransformPrimitiveMap : function(name){
		if(isset(name)){
			return this._transformPrimitiveMap[name];	
		}
		return this._transformPrimitiveMap;
	},
	/**
	 * 
	 * @param {!String} name
	 */
	getModel : function(name){
		if(isset(name)){
			return this._models[name];
		}
		return this._models;
	},
	/**
	 * 
	 * @param {!String} color
	 */
	setClearColor : function(color){
		this._clearColor = color;
	},
	/**
	 * 
	 * @param {!String} name
	 */
	createTransform : function(name){
		var transform = this._pack.createObject("Transform");
		transform.name = name;
		this._transforms[name] = transform;
		return transform;
	},
	/**
	 * 
	 * @param {String} name
	 * @param {String} materialName
	 * @param {Float} radius
	 * @param {Integer} subdivX
	 * @param {Integer} subdivY
	 */
	createSphere : function(name, materialName, radius, subdivX, subdivY){
		var sphere =o3djs.primitives.createSphere(this._pack, MaterialManager.getMaterial(materialName), radius, subdivX, subdivY);
		sphere.name = name;
		var transform = this.createTransform(name);
		transform.addShape(sphere);
		this._shapeTransformMap[sphere.name] = transform.name
		return transform;
	},
	/**
	 * 
	 * @param {String} name
	 * @param {String} materialName
	 * @param {Float} width
	 * @param {Float} depth
	 * @param {Integer} subdivX
	 * @param {Integer} subdivY
	 */
	createPlane : function(name, materialName, width, depth, subdivX, subdivY){
		var plane = o3djs.primitives.createPlane(this._pack, MaterialManager.getMaterial(materialName), width, depth, subdivX, subdivY);
		plane.name = name;
		var transform = this.createTransform(name);
		transform.addShape(plane);
		this._shapeTransformMap[plane.name] = transform.name;
		return transform;
	},
	/**
	 * 
	 * @param {String} name
	 * @param {String} materialName
	 * @param {Float} size
	 */
	createCube : function(name, materialName, size){
		var cube = o3djs.primitives.createCube(this._pack, MaterialManager.getMaterial(materialName), size);
		cube.name = name;
		var transform = this.createTransform(name);
		transform.addShape(cube);
		this._shapeTransformMap[cube.name] = transform.name;
		return transform;
	},
	
	createLoader : function(callback){
		this._loader = o3djs.loader.createLoader(callback);
		
		return this._loader;
	},
	/**
	 * 
	 * @param {String} name
	 * @param {String} path
	 * @param {String} cameraName the camera(viewInfo) name we want to bind the transform to
	 * @param {Function} callback
	 */
	loadModel : function(name, path, cameraName, callback){
		var transform = this.createTransform(name);
		
		this._loader.loadScene(this._client, this._pack, transform, path, function(pack, parent, exception){
			if(exception){
				error('Could not load: '+path + exception);
				return;
			}
			o3djs.pack.preparePack(pack, SceneManager.getCamera(cameraName).getViewInfo());
			SceneManager._transforms[name] = parent;
			//////////////////////////////////////////////////////////
			////fill the shapeTransformMap so we can find which transform shape belong to quickly
			//the structure of the model loaded from file
			//we will not consider the transform of each shape
			//			parent
			//				|
			//			"RootNode"
			//				|
			//		+---------------+
			//		|				|
			//	transform			transform
			//		|				|
			//		shape			shape
			//		|				|
			//	primitive			primitive
			var rootNode = parent.children[0];
			for (var i = 0; i < rootNode.children.length; i++) {
				var shapes = rootNode.children[i].shapes;
				if(shapes.length > 0){
					SceneManager._shapeTransformMap[shapes[0].name] = transform.name;
				}
			}
			//save the model loaded
			SceneManager._models[name] = path;
			//add transform to main camera defaultly
			parent.parent = SceneManager.getTransform("main_camera");

			callback(parent);
		})
	},
	/**
	 * bind the camera to a camera, since we may not have only one camera in the scene,
	 * like when we need shadow, we will put a camera at the light position; 
	 * @param {string} transformName
	 * @param {String} materialName
	 * @param {String} cameraName
	 * @param {boolean|string} option
	 */
	bindTransformToCamera : function(transformName, materialName, cameraName, option){
		option = isset(option) ? option : false;
		
		var material = MaterialManager.getMaterial(materialName);
		
		MaterialManager.bindCamera(materialName, cameraName, option);
		this.getTransform(transformName).createDrawElements(this._pack, material);
	},
	
	bindPrimitiveToCamera : function(primitiveName, materialName, cameraName, option){
		option = isset(option) ? option : false;
		
		var material = MaterialManager.getMaterial(materialName);
		
		MaterialManager.bindCamera(materialName, cameraName, option);
		this.getPrimitive(primitiveName).createDrawElement(this._pack, material);
	},
	/**
	 * add transform1 to transform2, of cource you can do it without it,
	 * just transform1.parent = trasnform2
	 * @param {String} transformName1
	 * @param {String} transformName2
	 */
	addTransformTo : function(transformName1, transformName2){
		this.getTransform(transformName1).parent = this.getTransform(transformName2);
	},
	/**
	 * add transform to the root of client, it is not recommanded to write
	 * transform.parent = ScenenManager._client.root directly;
	 * @param {String} transformName 
	 */
	addTransformToRoot : function(transformName){
		this.getTransform(transformName).parent = this._client.root;
	},
	/**
	 * list all primitives and prepare the primitiveShapeMap, shapePrimitiveMap, transformPrimitiveMap
	 * since we can not find a primitive through shape, find transform through shape or
	 * find primitive under the transform though
	 * o3d api directly
	 */
	listPrimitives : function(){
		var primitives = this._pack.getObjectsByClassName("o3d.Primitive");
		for(var i = 0; i < primitives.length; i++){
			//add map to shapePrimitiveMap
			var shapeName = primitives[i].owner.name;
			if(!isset(this._shapePrimitiveMap[shapeName])){
				this._shapePrimitiveMap[shapeName] = [];
			}
			this._shapePrimitiveMap[shapeName].push(primitives[i].name);
			//add map to primitiveShapeMap
			this._primitiveShapeMap[primitives[i].name] = shapeName;
			//add map to transformPrimitiveMap
			var transformName = this._shapeTransformMap[shapeName];
			if(!isset(this._transformPrimitiveMap[transformName])){
				this._transformPrimitiveMap[transformName] = [];
			}
			this._transformPrimitiveMap[transformName].push(primitives[i].name);
		}
	},
	
	getPrimitive : function(name){
		if(isset(name)){
			var primitives = this._pack.getObjects(name, "o3d.Primitive");
			return primitives[0];
		}
		return this._pack.getObjectsByClassName("o3d.Primitive");
	},
	
	getShape : function(name){
		if(isset(name)){
			var shapes = this._pack.getObjects(name, "o3d.Shape");
			return shapes[0];
		}
		return this._pack.getObjectsByClassName("o3d.Shape");
	},
	
	reset : function(){
		this._pack.destroy();
		this._lights = [];
		this._cameras = [];
		this._transforms = [];
		MaterialManager.reset();
		TextureManager.reset();
	},
	////////////////event handler
	/**
	 * 
	 * @param {String} name
	 * @param {Function} func
	 */
	appendRenderCallback : function(name, func){
		this._renderCallbackList[name] = func;
		this._updateRenderCallback();	
	},
	/**
	 * 
	 * @param {String} name
	 */
	removeRenderCallback : function(name){
		delete this._renderCallbackList[name];
		this._updateRenderCallback();
	},
	_updateRenderCallback : function(){
		for(var name in this._renderCallbackList){
			eval("var " + name + "=this._renderCallbackList[name]");
		}
		this._renderCallback = function(re){
			for (var name in SceneManager._renderCallbackList) {
				eval(name + "(re)");
			}
		}
		this._client.setRenderCallback(this._renderCallback);
	},
	/**
	 * 
	 * @param {String} event
	 * @param {Function} callback
	 */
	addEventListener : function(event, callback){
		o3djs.event.addEventListener(this._o3dElem, event, callback);
	}
}
